# Copyright (C) 2005-2006 Frederic Back <fredericback@gmail.com>
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by the
# Free Software Foundation; either version 2, or (at your option) any later
# version.
#
# This program is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTIBILITY
# or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
# for more details.

"""
This stuff should be touched with great care. Doing cairo operations
with pygtk is not easy to optimize: Python is the bottleneck. MUCH yet to
optimize and tweak.
"""

import cairo
import gobject
import gtk
import pango
import threading
import time

import GeoTypes
from GeoTypes import Box, Point
from _geodata import *
from _cursors import *

class MapCanvas(gtk.VBox):
    """ A widget to displays a map.
    The central element is a gtk.pixmap, where a Plotter draws on.

    Note: Zooming is computed in screen co-ordinates, and scaling is computed
    in world co-ordinates. This is stupid, but I don't care enough to change it.
    """

    # the canvas' background colour.
    # Note: This is hardcoded in pan mode (see onMouseMove)
    backgroundColour = 1.0,1.0,1.0

    __gsignals__ = {
        'box-selected': (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE,
        (gobject.TYPE_FLOAT,gobject.TYPE_FLOAT,gobject.TYPE_FLOAT,gobject.TYPE_FLOAT)),

        'point-selected': (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE,
        (gobject.TYPE_FLOAT,gobject.TYPE_FLOAT)),

        'view-changed': (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, ()),
    }

    def __init__(self):
        gtk.VBox.__init__(self)

        self.tool = None # see setMode()
        self.__state = None # save current tool

        self.plotter = None
        self.image = gtk.Image()
        self.layers = []
        self.layerQueue = []
        self.labelQueue = []

        self.invertmatrix = cairo.Matrix(1,0,0,1,0,0)
        self.matrix = cairo.Matrix(1,0,0,1,0,0)

        # zoom and pan
        self.zoomMatrix = cairo.Matrix(1,0,0,1,0,0)

        self.translateX = 0
        self.translateY = 0

        #----------------------------------------- add canvas and scrollbars
        self.table = gtk.Table(3, 3, False)
        self.pack_start(self.table)

        self.hadj = gtk.Adjustment(0, 0, 1, 0,0, 1)
        self.vadj = gtk.Adjustment(0, 0, 1, 0,0, 1)
        self.hadj.connect("value-changed", self.__scrollbarChanged)
        self.vadj.connect("value-changed", self.__scrollbarChanged)
        self.hscroll = gtk.HScrollbar(self.hadj)
        self.vscroll = gtk.VScrollbar(self.vadj)

        self.mapframe = gtk.EventBox()
        self.__createNewCanvas()
        self.mapframe.add(self.image)
        self.mapframe.set_events(gtk.gdk.POINTER_MOTION_MASK|gtk.gdk.POINTER_MOTION_HINT_MASK)

        self.scrollbarsLock = False
        self.scrollFromX = None
        self.scrollFromY = None

        esf = gtk.EXPAND|gtk.SHRINK|gtk.FILL
        self.table.attach(self.mapframe,1,2,1,2, esf,esf, 0,0)
        self.table.attach(self.hscroll, 1, 2, 2, 3, esf,0, 0, 0 )
        self.table.attach(self.vscroll, 2, 3, 1, 2, 0, esf, 0, 0 )

        # add a statusbar
        self.statusbar = gtk.Statusbar()
        self.statusbar.set_has_resize_grip(True)
        self.statusbar.sid = self.statusbar.get_context_id("canvas")
        self.pack_end(self.statusbar,False)

        # add two labels to write co-ordinates
        self.statusbar.xcoord = gtk.Entry()
        self.statusbar.pack_start(self.statusbar.xcoord,False)
        self.statusbar.ycoord = gtk.Entry()
        self.statusbar.pack_start(self.statusbar.ycoord,False)

        self.show_all()
        self.__resizestart = None
        self.__timeOut = None

        self.mapframe.connect("size-allocate",self.__onResize)
        self.mapframe.connect("motion-notify-event",self.__onMouseMove)
        self.mapframe.connect("button-press-event",self.__onButtonPress)
        self.mapframe.connect("button-release-event",self.__onButtonRelease)
        self.mapframe.connect('scroll-event', self.__onScrollEvent)

        self.mapframe.set_events( gtk.gdk.EXPOSURE_MASK
                                | gtk.gdk.LEAVE_NOTIFY_MASK
                                | gtk.gdk.BUTTON_PRESS_MASK
                                | gtk.gdk.POINTER_MOTION_MASK )


    def setCurrentTool(self, tool):
        """ Every tool triggers a different 'mode'. """

        self.tool = tool
    
        # switch cursor
        if tool=="pan":
            self.mapframe.window.set_cursor(getCursor("hand-open"))
        elif tool=="box":
            self.mapframe.window.set_cursor(gtk.gdk.Cursor(gtk.gdk.CROSSHAIR))
        elif tool in ["pointer",None]: # default mode
            self.mapframe.window.set_cursor( gtk.gdk.Cursor(gtk.gdk.CROSSHAIR) )
        else: raise ValueError, "Unknown mode '%s'."%mode 

    def resetZoom(self):
        """ Zoom to full extent """
        self.zoomMatrix = cairo.Matrix(1,0,0,1,0,0)
        self.translateX = 0
        self.translateY = 0
        self.updateZoom( 0.95 ) # a touch of zoom to correct size problems

    def updateZoom(self, factor = 1.0 ):
        """ Zoom by a factor """
        tx = (self.width - (self.width * factor)) / 2
        ty = (self.height - (self.height * factor)) / 2
        self.zoomMatrix *= cairo.Matrix(factor,0,0,factor,tx,ty)
        self.redrawLayers()
        self.__adjustScrollbars()
        self.emit("view-changed")

    def zoomFrom(self, x, y, factor):
        """ Set the center of the map at x,y, then zoom by factor """
        cx, cy = self.invertmatrix.transform_point(self.width/2,self.height/2)
        self.translateX += cx-x
        self.translateY += cy-y
        self.updateZoom(factor)

    def getCurrentViewExtent(self):
        """ Get the currently visible area in world co-ordinates. """
        x1,y1 = self.invertmatrix.transform_point(0,self.height)
        x2,y2 = self.invertmatrix.transform_point(self.width,0)
        #if x2<x1: x1,x2 = x2,x1
        #if y2<y1: y1,y2 = y2,y1
        return (x1,y1,x2,y2)

    def zoomToExtent(self, ox1, oy1, ox2, oy2):
        """ Zoom to an extent (in world co-ordinates) """

        if abs(ox2-ox1) == 0 or abs(oy2-oy1) == 0:
            return self.zoomFrom(ox2,oy2,2.0) # zoom from the last point!

        # swap
        if ox2 < ox1: ox1,ox2 = ox2,ox1
        if oy2 < oy1: oy1,oy2 = oy2,oy1

        # transform world co-ordinates to image co-ordinates
        # Note that y1 and y2 are swapped because y2 is larger than y1
        x1,y2 = self.matrix.transform_point(ox1,oy1)
        x2,y1 = self.matrix.transform_point(ox2,oy2)

        #print "----------------------------------------------------------------"
        #print (0,0),"->",(self.width,self.height)
        #print "(%i,%i) -> (%i,%i)"%(x1,y1,x2,y2)

        # scale to highest ratio
        (pw, ph) = (float(self.width), float(self.height))
        (ww, wh) = (x2-x1, y2-y1)
        imageRatio = ph/pw
        worldRatio = wh/ww
        tx, ty = 0,0
        if imageRatio > worldRatio:
            screenh = pw * wh / ww
            screenw = pw
            ty = (ph - screenh) / 2
        else:
            screenh = ph
            screenw = ph * ww / wh
            tx = (pw - screenw) / 2

        #get scale factors
        sx = screenw / ww
        sy = screenh / wh

        scale = cairo.Matrix( sx, 0, 0, sy, tx, ty )
        self.zoomMatrix *= scale

        # transform by world co-ordinates
        cx, cy = self.invertmatrix.transform_point(0,0)
        self.translateX += cx-ox1
        self.translateY += (cy-oy1)-(oy2-oy1)

        # redraw
        self.redrawLayers()
        self.__adjustScrollbars()
        self.emit("view-changed")

    def addLayer(self, layer):
        """ Add a Layer to the list of Layers to display """
        if len(self.layers) > 0: # if other layers already exist...
            self.layers.append(layer)
            self.layerQueue.append(layer)
            if len(self.layerQueue) == 1: self.__drawNextLayer()
        else:
            self.setLayers([layer])


    def setLayers(self, layers):
        """ Set a list of Layer instances to display, and start drawing. """

        if len(layers) == 0: return self.clearLayers()
        self.layers = layers

        self.extent = None
        self.resetZoom()

    def clearLayers(self):
        """ remove all layers. """
        self.extent = None
        self.layers = []
        self.layerQueue = []
        self.labelQueue = []
        self.redrawLayers()
     
    def stop(self):
        """ Stop drawing. """    
        if self.plotter is None: return
        try:
            if self.plotter.isAlive(): self.plotter.stop()
            self.plotter = None
        except Exception, e:
            print "Error while stopping the plotter:",e


    def redrawImageSignal(self, plotter):
        """ Tell our gtk.image we would like it to redraw. """
        if plotter == self.plotter: self.image.queue_draw()


    def redrawLayers(self):

        # print "redraw all layers-----------------------------------------------"

        self.__createNewCanvas()
        if len(self.layers) > 0:

            # get self.extent
            if self.extent is None:
                for layer in self.layers:
                
                    # try to get extent from query
                    if self.extent == None:
                        self.extent = layer.extent
                    elif layer.extent is not None:
                        self.extent = joinBoxes( self.extent, layer.extent )
                        
            #print " - map extent:"+str(self.extent)

            # recompute matrix
            self.__computeMatrix(self.extent)

            # queue layers
            # Note: the labelqueue gets filled in the _drawNextLayer routine.
            self.layerQueue = [] + self.layers
            self.labelQueue = []
            self.__drawNextLayer()


    def __adjustScrollbars(self):
        """ Call when changing transformation matrices  """

        self.scrollbarsLock = True # avoid calling __scrollbarChanged
        x, y = self.invertmatrix.transform_point(0,0)    
        tx, ty = self.invertmatrix.transform_point(self.width,self.height)    
        Mx, My = tx-x, y-ty
        self.scrollFromX, self.scrollFromY = x, -y

        self.hadj.set_all( value=x,
            lower = self.worldMinX, upper = self.worldMaxX,
            step_increment=Mx/100,page_increment=Mx/10,page_size=Mx)

        # Note: Flip y values.
        self.vadj.set_all( value= -y,
            lower = -self.worldMaxY, upper = -self.worldMinY,
            step_increment=My/100,page_increment=My/10,page_size=My)

        self.scrollbarsLock = False

    #=========================================================== private methods

    def __onMouseMove(self, widget, e):
        # show current co-ordinates in the status bar
        x, y = self.invertmatrix.transform_point(e.x,e.y)
        self.statusbar.xcoord.set_text(str(x)+"x")
        self.statusbar.ycoord.set_text(str(y)+"y")
        if self.__state == "pan":
            tx = int(e.x - self.dragFromX)
            ty = int(e.y - self.dragFromY)
            self.tgc.set_rgb_fg_color(gtk.gdk.Color(65535,65535,65535))
            self.tpix.draw_rectangle(self.tgc,True,0,0,self.width,self.height)
            self.tpix.draw_drawable(self.tgc,self.pixmap,0,0,tx,ty,-1,-1)
            # draw a line from origin to current point
            #self.tgc.set_rgb_fg_color(gtk.gdk.Color(0,0,0))
            #self.tpix.draw_line(self.tgc,e.x,e.y, int(e.x-tx), e.y-ty )
            self.image.queue_draw()
        elif self.__state == "box":
            # TODO: move this box code, and the pan display code somewhere external

            # remove box
            try: self.tpix.draw_drawable(self.tgc,self.pixmap,*self.tpix.redrawCoords)
            except: pass

            # draw box
            x = int(min(e.x,self.dragFromX))
            y = int(min(e.y,self.dragFromY))
            w = int(max(e.x,self.dragFromX) - x)
            h = int(max(e.y,self.dragFromY) - y)

            # looks better than rectangle because of dashing
            self.tpix.draw_line(self.tgc,x,y,x,y+h)
            self.tpix.draw_line(self.tgc,x,y,x+w,y)
            self.tpix.draw_line(self.tgc,x+w,y,x+w,y+h)
            self.tpix.draw_line(self.tgc,x,y+h,x+w,y+h)

            # remember currently drawn box co-ordinates
            self.tpix.redrawCoords = (x,y,x,y,w+1,h+1)
            self.image.queue_draw()
        return True

    def __onScrollEvent(self, widget, e):
        if e.direction == gtk.gdk.SCROLL_UP:
            self.updateZoom(2)
        elif e.direction == gtk.gdk.SCROLL_DOWN:
            self.updateZoom(0.5)

    def __onButtonPress(self, widget, e):
        if e.button == 1 and self.tool == "pan":
            self.stop()
            self.__state = "pan"
            self.mapframe.window.set_cursor( getCursor("hand-closed") )

            self.dragFromX = e.x
            self.dragFromY = e.y
            self.tpix = gtk.gdk.Pixmap(None,self.width,self.height,24)
            self.tgc = self.pixmap.new_gc()
            self.tgc.set_rgb_fg_color(gtk.gdk.Color(65535,65535,65535))
            self.tpix.draw_drawable(self.tgc,self.pixmap,0,0,0,0,-1,-1)
            self.image.set_from_pixmap(self.tpix,None)
        elif e.button == 1 and self.tool == "box":
            self.stop()
            self.__state = "box"
            self.dragFromX = e.x
            self.dragFromY = e.y
            self.tpix = gtk.gdk.Pixmap(None,self.width,self.height,24)
            self.tgc = self.pixmap.new_gc()
            self.tgc.set_values(line_style=gtk.gdk.LINE_ON_OFF_DASH)
            self.tgc.set_dashes(0, (2,2))
            self.tpix.draw_drawable(self.tgc,self.pixmap,0,0,0,0,-1,-1)
            self.image.set_from_pixmap(self.tpix,None)
        elif (e.button == 1 and self.tool == "pointer")\
          or (e.button == 3):
            x,y = self.invertmatrix.transform_point(e.x,e.y)
            self.emit("point-selected",x,y)
        

    def __onButtonRelease(self, w, e):
        if self.__state == "pan":
            self.mapframe.window.set_cursor( getCursor("hand-open") )
            dx,dy = self.invertmatrix.transform_point(self.dragFromX,self.dragFromY)
            ex,ey = self.invertmatrix.transform_point(e.x,e.y)
            self.translateX += ex-dx
            self.translateY += ey-dy
            self.redrawLayers()
            self.__adjustScrollbars()
            self.emit("view-changed")
            self.__state = None # reset state
        elif self.__state == "box":
            dx,dy = self.invertmatrix.transform_point(self.dragFromX,self.dragFromY)
            ex,ey = self.invertmatrix.transform_point(e.x,e.y)
            #print "from",dx,",",dy
            #print "to",ex,",",ey
            self.image.set_from_pixmap(self.pixmap,None)
            self.__state = None # reset state
            self.emit("box-selected",dx,dy,ex,ey)

    def __scrollbarChanged(self, adj):
        # lock can be turned on to avoid adjusting the scrollbar
        if not self.scrollbarsLock:
           
            self.translateX += self.scrollFromX - self.hadj.get_value()
            self.translateY -= self.scrollFromY - self.vadj.get_value()
            self.scrollFromX = self.hadj.get_value()
            self.scrollFromY = self.vadj.get_value()

            self.__resizestart = time.time() + 0.2
            if self.__timeOut is None:
                self.__timeOut = gobject.timeout_add(1, self.__resizeTimeout)
                print "resizing started: ",self.__resizestart


    def __computeMatrix(self, extent):
        """ Create a matrix to transform world units to canvas
        units. Returns a cairo transformation matrix.

        Postgis results are flipped upside down.
            a   b   u      1 0 0
            c   d   v      0 1 0
            x0  y0  w      0 0 1

        cairo.Matrix( a   c   b   d  x0  y0)
        cairo.Matrix( 1,  0,  0,  1,  0,  0)

        x' =  sx*x + c*y  + x0
        y' =  b*x + sy*y  + y0 """

        try:
            pw = screenwidth  = float(self.width) # pixmap width
            ph = screenheight = float(self.height) # pixmap height

            ll = extent.getLowerLeft()
            ur = extent.getUpperRight()
            ww = float(ur.getX() - ll.getX()) # world width
            wh = float(ur.getY() - ll.getY()) # world height

            # fit on pixmap by comparing ratios
            # use translateX and translateY to center on screen

            screenratio = ph/pw # ratio of pixmap
            wratio = wh / ww    # ratio of geometry
            translateX, translateY = 0,0
            if screenratio > wratio:
                screenh = pw * wh / ww
                screenw = pw
                translateY = (ph - screenh) / 2
            else:
                screenh = ph
                screenw = ph * ww / wh
                translateX = (pw - screenw) / 2

            # get scale factors
            sx = screenw / ww
            sy = screenh / wh
            
            trans = cairo.Matrix( 1, 0, 0, 1, -ll.getX(), -ll.getY()-wh )
            scale = cairo.Matrix( sx, 0, 0, -sy, 0, 0 )

            # set world matrix
            self.matrix = cairo.Matrix(1,0,0,1,self.translateX,self.translateY)
            self.matrix *= trans * scale
            self.matrix *= cairo.Matrix(1, 0, 0, 1, translateX, translateY)
            self.matrix *= self.zoomMatrix

            # Hack. Compute inverse matrix here. Can't deepcopy a cairo.Matrix
            self.invertmatrix = cairo.Matrix(1,0,0,1,self.translateX,self.translateY)
            self.invertmatrix *= trans * scale
            self.invertmatrix *= cairo.Matrix(1,0,0,1, translateX, translateY)
            self.invertmatrix *= self.zoomMatrix
            self.invertmatrix.invert()

            # ------------------------------------------------------------------------
            # One more hack. Compute visible world when fully zoomed out
            # This is used for the scrollbars
            m = trans * scale
            m *= cairo.Matrix(1,0,0,1, translateX, translateY)
            m.invert()     
            self.worldMinX,self.worldMaxY = m.transform_point(0,0) # Y is flipped!
            self.worldMaxX,self.worldMinY = m.transform_point(self.width,self.height)           
            
            # now compare with current view, take the most extreme value
            mx,My = self.invertmatrix.transform_point(0,0)    
            Mx,my = self.invertmatrix.transform_point(self.width,self.height)    
            self.worldMinX = min(mx,self.worldMinX)
            self.worldMinY = min(my,self.worldMinY)            
            self.worldMaxX = max(Mx,self.worldMaxX)
            self.worldMaxY = max(My,self.worldMaxY)
            #print "x: %i < %i"%(self.worldMinX,self.worldMaxX)
            #print "y: %i < %i"%(self.worldMinY,self.worldMaxY)
            return True # ok

        except Exception, e:
            print "Error while building matrix for %s"%extent
            print "  %s"%e
            print "  Fall back to -1000,-1000 to 1000,1000"
            self.__computeMatrix( Box(Point(-1000,-1000),Point(1000,1000)) )
            return False # tell the caller we have a borked matrix


    def __createNewCanvas(self):
        """ create a new, blank pixmap to draw on. """
        self.stop()

        # adapt width and height to mapframe size
        aw = self.mapframe.get_allocation()
        self.width, self.height = aw.width, aw.height

        # create a new pixmap and cairo context
        self.pixmap = gtk.gdk.Pixmap(None,self.width,self.height,24)
        self.image.set_from_pixmap(self.pixmap,None)
        self.ctx = self.pixmap.cairo_create()
        
        # clear the canvas
        self.ctx.rectangle(0,0,self.width,self.height)
        self.ctx.set_source_rgb(*MapCanvas.backgroundColour)
        self.ctx.fill()   

        # test pango stuff
        #self.ctx.move_to(self.width/2, self.height/2)
        #self.ctx.set_source_rgb(0,0,0)
        #self.pango = self.ctx.create_layout()
        #self.pango.set_text("hello")
        #self.ctx.show_layout(self.pango)


    def __drawNextLayer(self):
        """ Send the next Layer in the stack to the plotter. """
        if len(self.layerQueue) > 0:
            layer = self.layerQueue.pop()
            self.labelQueue.append(layer)
            self.plotter = Plotter(self.ctx, layer, self.matrix)
            self.plotter.redrawCallback = self.redrawImageSignal
            self.plotter.cb_done = self.__drawNextLayer
            self.plotter.start()
            
        # after plotting the geometry, plot the labels
        elif len(self.labelQueue) > 0:
            layer = self.labelQueue.pop()
            #print "layerQueue empty, draw layer %s"%layer.name
            self.plotter = LabelPlotter(self.ctx, layer, self.matrix)
            self.plotter.redrawCallback = self.redrawImageSignal
            self.plotter.cb_done = self.__drawNextLayer
            self.plotter.start()
    
    def __onResize(self, widget, event):
        # Terrible hack: ignore first call
        if self.__resizestart is None:
            self.__resizestart = 1
            return

        # onResize will be called a lot when resizing manually. '''  
        if event.width/10 != self.width/10 or event.height/10 != self.height/10:
            self.__resizestart = time.time() + 0.2
            if self.__timeOut is None:
                self.__timeOut = gobject.timeout_add(5, self.__resizeTimeout)
                print "resizing started: ",self.__resizestart

    def __resizeTimeout(self):
        if time.time() > self.__resizestart:
            self.redrawLayers()
            self.__timeOut = None
            print "resize done"
            return False
        return True

gobject.type_register(MapCanvas)



#-------------------------------------------------------------------------------

class LabelPlotter(threading.Thread):
    """ A quick hackish copy of the plotter, made to display labels """

    def __init__(self, cairocontext, layer, matrix):
        threading.Thread.__init__(self)
        self.ctx = cairocontext
        self.layer = layer
        self.worldMatrix = matrix
        self.redrawCallback = None # callback func to redraw image
        self.cb_done = None        # call when finished (not if stopped)
        self._quit = False

    def stop(self):
        self.cb_done = None 
        self.redrawCallback = None
        self._quit = True

    def setupStyle(self):
        # prepare context depending on geometry type to reduce cairo operations
        # Note: Everything must be explicitly set

        self.outlineColour = self.layer.style.outline.asCairoRGBA()
        self.lineColour = self.layer.style.line.asCairoRGBA()
        self.fillColour = self.layer.style.fill.asCairoRGBA()

        self.ctx.set_line_cap(cairo.LINE_CAP_BUTT)
        self.ctx.set_antialias(cairo.ANTIALIAS_DEFAULT)
        self.ctx.set_line_width(self.layer.style.linewidth)
        if self.layer.geometryType == "point":
            self.ctx.set_line_cap(cairo.LINE_CAP_SQUARE)
            #self.ctx.set_antialias(cairo.ANTIALIAS_NONE)   

        self.symbol = SymbolRenderer() 
        self.symbol.recreate(self.layer.style.line,
            self.layer.style.outline, self.layer.style.fill,
            self.layer.style.symbol,self.layer.style.symbolsize)
            
        self.labelcolumn = self.layer.style.labelcolumn
        self.labelFlag = self.layer.style.labelFlag
        self.labelsyntax = self.layer.style.labelsyntax
        self.labelfontdes = pango.FontDescription(self.layer.style.labelfont)
        self.labelcolour = self.layer.style.labelcolour.asCairoRGB()
        
    def run(self):
        magicNumber = 400

        #print "plotting labels for layer %s"%self.layer.name

        if self.layer == None or len(self.layer.geomCols) == 0:
            if self.cb_done: self.cb_done()      
            return

        self.setupStyle()
        if not self.labelFlag or self.labelcolumn < 0:
            if self.cb_done: self.cb_done()
            return

        fetchindex = 0
        iterations = 0
        starttime = time.time()
        label = None
        while fetchindex < self.layer.totalsize:
            if self._quit == True: break;

            # TODO: remove that! Find a better logic to that block.
            time.sleep(0.1)
            for i in range( magicNumber ):

                if fetchindex < self.layer.size and fetchindex < self.layer.totalsize:

                    v = self.layer.get_data(fetchindex,self.labelcolumn)
                    try: label = self.labelsyntax%v
                    except: label = None
                        
                    # loop through every geometry in that row.
                    for col in self.layer.geomCols:
                        g = self.layer.getGeometry(fetchindex,col)   
                        try:  
                            gtk.gdk.threads_enter()
                            if label is not None: self.__plotlabel(g,label)
                            gtk.gdk.threads_leave()
                        except Exception, e:
                            print e

                    fetchindex += 1

            if self._quit == True: break
            gtk.gdk.threads_enter()
            if self.redrawCallback: self.redrawCallback(self)
            gtk.gdk.threads_leave()
            iterations += 1

        endtime = time.time() - starttime
        print 'labelled "%s" (%i shapes, %ssec, %i iterations)'%(self.layer.name,
            self.layer.totalsize,round(endtime,2),iterations) 
            
        gtk.gdk.threads_enter()
        if self.cb_done: self.cb_done()
        gtk.gdk.threads_leave()

    def __get_first_point(self, g):
        """ Return the first point of a geometry as a tuple (x,y). """
        if g == None: return (None,None)
        n = g.__class__.__name__
        if n == "OGPoint":
            return g._x,g._y
        elif n[:6] == "OGLine": # linestrings and linear rings...
            return self.__get_first_point(g._points[0])
        elif n == "OGPolygon":
            return self.__get_first_point(g._lines[0])
            
        elif n[:7] == "OGMulti" or n == "OGGeometryCollection":
            return self.__get_first_point(g.getGeometries()[0])
        else:
            print n
            return (None,None) 

    def __plotlabel(self, g, label):
        (x,y) = self.__get_first_point(g)
        if not x or not y: return
        p = self.worldMatrix.transform_point(x,y)
            
        self.ctx.move_to(p[0],p[1])
        self.ctx.set_source_rgb(*self.labelcolour)
        pango = self.ctx.create_layout()
        pango.set_font_description(self.labelfontdes)
        pango.set_text(str(label))
        self.ctx.show_layout(pango)
        




class Plotter(threading.Thread):
    """ One of the fundamental problems in mezoGIS is that due to the relative
        slowness of the database retrieval via psycopg (as opposed to other languages),
        I need to treat icoming data as a stream. """

    printGeometries = False

    def __init__(self, cairocontext, layer, matrix):
        threading.Thread.__init__(self)

        self.ctx = cairocontext
        self.layer = layer
        self.worldMatrix = matrix

        # callbacks and signals
        self.redrawCallback = None # signal to redraw image
        self.cb_done = None        # call when finished (not if stopped)
        self._quit = False

    def stop(self):
        self.cb_done = None 
        self.redrawCallback = None
        self._quit = True

    def setupStyle(self):
        # prepare context depending on geometry type to reduce cairo operations
        # Note: Everything must be explicitly set

        self.outlineColour = self.layer.style.outline.asCairoRGBA()
        self.lineColour = self.layer.style.line.asCairoRGBA()
        self.fillColour = self.layer.style.fill.asCairoRGBA()

        self.ctx.set_line_cap(cairo.LINE_CAP_BUTT)
        self.ctx.set_antialias(cairo.ANTIALIAS_DEFAULT)
        self.ctx.set_line_width(self.layer.style.linewidth)
        if self.layer.geometryType == "point":
            self.ctx.set_line_cap(cairo.LINE_CAP_SQUARE)
            #self.ctx.set_antialias(cairo.ANTIALIAS_NONE)   

        self.symbol = SymbolRenderer() 
        self.symbol.recreate(self.layer.style.line,
            self.layer.style.outline, self.layer.style.fill,
            self.layer.style.symbol,self.layer.style.symbolsize)
            
        self.labelcolumn = self.layer.style.labelcolumn
        self.labelFlag = self.layer.style.labelFlag
        self.labelsyntax = self.layer.style.labelsyntax

    def run(self):
        magicNumber = 400

        if self.layer == None or len(self.layer.geomCols) == 0:
            if self.cb_done: self.cb_done()      
            return

        fetchindex = 0
        iterations = 0
        starttime = time.time()
        setupDone = False # flag to check wheter setupStyle has been called
        while fetchindex < self.layer.totalsize:
            if self._quit == True: break;

            # TODO: remove that! Find a better logic to that block.
            #time.sleep(0.1)
            if fetchindex >= self.layer.size: time.sleep(0.1)
            
            for i in range( magicNumber ):

                if fetchindex < self.layer.size and fetchindex < self.layer.totalsize:
                
                    # look at the layer's style only at first iteration 
                    if not setupDone:
                        self.setupStyle()
                        setupDone = True
                            
                    # loop through every geometry in that row.
                    for col in self.layer.geomCols:
                        g = self.layer.getGeometry(fetchindex,col)   
                        try:
                            gtk.gdk.threads_enter()
                            g.draw(g,self)
                            gtk.gdk.threads_leave()
                        except Exception, e:
                            print e

                    fetchindex += 1

            if self._quit == True: break
            
            # make callback depend on time
            
            gtk.gdk.threads_enter()
            if self.redrawCallback: self.redrawCallback(self)
            gtk.gdk.threads_leave()
            
            iterations += 1

        endtime = time.time() - starttime
        print 'plotted "%s" (%i shapes, %ssec, %i iterations)'%(self.layer.name,
            self.layer.totalsize,round(endtime,2),iterations) 

        gtk.gdk.threads_enter()
        if self.cb_done: self.cb_done()
        gtk.gdk.threads_leave()



#-------------------------------------------------------- new plotting functions
# this approach is *slightly* faster than the old one

def plot_linestring(ogclass, g, plotter):
    first = True
    for point in g._points:
        p = plotter.worldMatrix.transform_point(point._x,point._y)
        if first: plotter.ctx.move_to(p[0],p[1])
        else: plotter.ctx.line_to(p[0],p[1])
        first = False

    plotter.ctx.set_source_rgba(*plotter.lineColour)
    plotter.ctx.stroke()

def plot_point(ogclass, point, plotter):
    p = plotter.worldMatrix.transform_point(point._x,point._y)
    plotter.ctx.save()
    plotter.ctx.translate(p[0],p[1])
    plotter.symbol.render(plotter.ctx)
    plotter.ctx.restore()
    
def plot_polygon(ogclass, g, plotter):
    for line in g._lines:
        first = True
        for point in line._points:
            p = plotter.worldMatrix.transform_point(point._x,point._y)
            if first: plotter.ctx.move_to(p[0],p[1])
            else: plotter.ctx.line_to(p[0],p[1])
            first = False

    plotter.ctx.set_source_rgba(*plotter.fillColour)
    plotter.ctx.fill_preserve()
    plotter.ctx.set_source_rgba(*plotter.outlineColour)
    plotter.ctx.stroke()

def plot_multi(ogclass, g, plotter):
    for subg in g.getGeometries():
        subg.draw(subg, plotter)

#-------------------------------------------------------------------------------
def register_drawing_callbacks():
    GeoTypes.OGPoint.draw = plot_point
    GeoTypes.OGLineString.draw = plot_linestring
    GeoTypes.OGPolygon.draw = plot_polygon
    GeoTypes.OGMultiLineString.draw = plot_multi
    GeoTypes.OGMultiPoint.draw = plot_multi
    GeoTypes.OGMultiPolygon.draw = plot_multi
    GeoTypes.OGGeometryCollection.draw = plot_multi
    
register_drawing_callbacks()

